module ActiveDirectory
  class Connection
    attr_accessor :host, :port

    def raw
      @connection
    end

    def log(facility, message)
      ActiveDirectory::Base.log(facility, message)
    end

    def initialize(host, port)
      @connection = nil
      @method = nil

      @host = host || 'localhost'
      @port = port || 389
      
      @username = nil
      @password = nil
      @allow_anonymous_bind = false
    end

    def clone
      self.class.new(@host,@port)
    end

    def ensure_connectivity(number_of_retries = 3)
      connect(@username,@password) unless @connection
      raise RuntimeError('Unable to ensure connectivity!') unless @connection
      retries = 0
      begin
        yield @connection
      rescue
        raise
#       raise if retries == number_of_retries
#       retries += 1
#       reconnect
#       retry
      end
    end

    def connect(username, password, allow_anonymous_bind = true)
      @username = username
      @password = password
      @allow_anonymous_bind = allow_anonymous_bind
      begin
        @method.nil? ? connect_initially : connect_to_bind
      rescue Exception => e
        log(:debug, "Rescued from thrown error: #{e.message}")
        @connection = nil
        raise
      end
    end
    
    def reconnect
      close and connect
    end
    
    def close
      begin
        @connection.unbind unless @connection.nil?
      rescue
      end
      @connection = nil
    end

    def connect_to_bind
      raise(RunTimeError, 'No method is available for connecting') if @method.nil?
      
      @connection = @method.call(@host, @port)
      @connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
      @connection.set_option(LDAP::LDAP_OPT_REFERRALS, 0)
      
      # Attempt to bind with a username and password
      begin
        @connection.bind(@username, @password)
      rescue
        if @allow_anonymous_bind
          # Attempt to bind anonymously
          @connection.bind()
        else
          raise
        end
      end
    end

    def connect_initially
      # Attempt to connect using TLS
      begin
        @method = Proc.new { |host,port| LDAP::SSLConn.new(host,636,true) }
        log :debug, "Trying to connect over SSL with TLS"
        connect_to_bind
        log :debug, "Connected over SSL with TLS"
        return
      rescue
        # If TLS fails, try using SSL
        begin
          @method = Proc.new { |host,port| LDAP::SSLConn.new(host,636,false) }
          log :debug, "Trying to connect over SSL without TLS"
          connect_to_bind
          log :debug, "Connected over SSL without TLS"
          return
        rescue
          # If SSL fails too, try an unencrypted connection
          begin
            log :debug, "Trying to connect without encryption."
            @method = Proc.new { |host, port| LDAP::Conn.new(host,port) }
            connect_to_bind
            log :debug, "Connected without encryption"
            return
          rescue
            # If all else fails, give up on trying to connect
            log :debug, "Unable to connect to Domain Controller"
            @method = nil
            @connection = nil
            raise
          end
        end
      end
    end
  end
end
